/// Author: QP
/// Version: 1.2
/// Date:2023/03/26
/// 电子病历文档校验工具类
/// 基于标准对电子病历文档XML进行校验
Class HCC.Utils.DocValid Extends %RegisteredObject
{

/// <p><b>校验文档文件</b>
/// <br> pFile: JSON文件路径。 测试阶段传入已经转换为JSON的互联互通文档路径，今后会先调用python的XML2JSON将互联互通文档转为JSON。
/// <br> pCheckDisplayName: 是否检查文档元素的@displayName，默认不检查
/// <br> 文档校验错误信息保存在^HCC.DocCheckLog,第一维下标是文档编号
/// 它基于文档模版global。这些global命名规范为：^HCC.DocTemplate.<文档类型>， 例如^HCC.DocTemplate.C0032
/// 使用示例：
/// <br> Do ##class(HCC.Utils.DocValid).CheckDoc("/HCCDoc/C0032.json",1)
ClassMethod CheckDocFile(pFile As %String = "/Users/qiaopeng/irishealth/HCC/sampleDoc/C0019.json", pCheckDisplayName As %Boolean = 0) As %String
{
    #define EndWith(%1,%2) ($E(%1,($L(%1)-$L(%2)+1),$L(%1))=%2)
    #define BeginWith(%1,%2) ($E(%1,1,$L(%2))=%2)
    Set tRet = ""
    Try
    {
    // 不检查@displayName
    Set %tCheckDisplayName = pCheckDisplayName
    
    /*
    Set tStream=##class(%Stream.FileCharacter).%New()
    Set sc=tStream.LinkToFile(pFile)
    Set tStream.TranslateTable="UTF8"
    Set tLen = 3200000
    While 'tStream.AtEnd 
    {
        Set tXMLStr=tStream.Read(.tLen)
    }
    Do tStream.%Close()
    Set tJSONStr = ..XML2JSON(tXMLStr)
    */

    Set stream=##class(%Stream.FileCharacter).%New()
    Set sc=stream.LinkToFile(pFile)
    Set stream.TranslateTable="UTF8"
    Set tLen = 3200000
    While 'stream.AtEnd 
    {
        Set tJSONStr=stream.Read(.tLen)
    }

    Set tDocJSONObj = {}.%FromJSON(tJSONStr)

    /// 获得文档类型和文档序列号，并使其变量全局可用
    /// %tDocType：文档类型
    /// %tDocSN：文档序列号
    /// %tCurrentSection: 当前处理的章节
    /// %tDocTemplate： 文档模版名
    Set %tDocType = tDocJSONObj.ClinicalDocument.code."@code", %tDocSN = tDocJSONObj.ClinicalDocument.id."@extension"
    Set tRegex=##class(%Regex.Matcher).%New("C{1}0{2}([0-9]{2})")
    Set tRegex.Text=%tDocType
    Set tCorrectDocType = tRegex.Locate()
    If tCorrectDocType
    {
        Set tDocTypeNumber = +tRegex.SubstituteIn("$1")
        Set:((tDocTypeNumber>53) || (tDocTypeNumber<1)) tCorrectDocType = 0
    }
    W:(tCorrectDocType = 0) "错误的文档类型：",%tDocType,!
    // 获得文档模版global名，并使其变量全局可用
    Set %tDocTemplate = "^HCC.DocTemplate."_%tDocType
    K ^||TempTree,^||DocCheckErr

    Set %tCurrentSection=""

    // 遍历文档，并按模版检查文档元素
    Do ..LoopCheckDocument(tDocJSONObj,"")

    // 遍历模版，并检查文档元素Cardinality
    Set tSub= ""
    Do
    {
        Set tSub=$O(@%tDocTemplate@(tSub))

        // 不检查以下元素的Cardinality
        Set tNeedCheck=1, tSkipElements = "code|value|name|qualifier|performer|effectiveTime"       
        If $F(tSkipElements,$P(tSub,"|",$L(tSub,"|")))>0
        {
            Set tNeedCheck = 0
        }
        Continue:(tNeedCheck = 0)

        Set tAskedCard = $G(@%tDocTemplate@(tSub,"Card")),tActualCard = $G(^||TempTree(tSub,"Card"))
        Set:(tAskedCard="") tAskedCard="1..1" 
        Set tAskedMin = $Case(tAskedCard,"0..1":0,"0..*":0,:1)
        Set tAskedMax = $Case(tAskedCard,"0..*":100,"1..*":100,:1)
        Set:(tActualCard="") tActualCard=0

        If (tActualCard<tAskedMin)
        {
            Set ^||DocCheckErr(%tDocSN,tSub) = "基数错误-缺失^缺失:"_tSub
        }
        Elseif (tActualCard>tAskedMax)
        {
            Set ^||DocCheckErr(%tDocSN,tSub) = "基数错误-重复^出现"_tActualCard_"次, 超过要求的："_tAskedCard
        }
    
    } While tSub '=""
    

    // 输出错误信息
    ;k ^HCC.DocCheckLog
    Set tSub= $O(^||DocCheckErr(%tDocSN,"")), (tPreErr,tCurErr) = "", tErrCount=0, tErrJson={}
    set:(tSub'="") tErrJson.Msg=[]
    While tSub '=""
    {
        Set tCurErr = $P($G(^||DocCheckErr(%tDocSN,tSub)),"^")
        
        If (((tCurErr="基数错误-缺失")||(tCurErr="基数错误-重复")||(tCurErr="非标文档元素")) && (tCurErr '= tPreErr)) || (tCurErr="文档元素错误")||(tCurErr="数据元错误")
        {
            // 特殊处理：C0019的器械物品核对章节，无法区别"术前"、"关前"、"关后", 因此会出现3次，这是正常的。无需处理
            If (%tDocType="C0019")&&(tCurErr="基数错误-重复")&&($$$EndWith(tSub,"section|entry|DE08.50.042.00"))&&($$$BeginWith($P($G(^||DocCheckErr(%tDocSN,tSub)),"^",2),"出现3次"))
            {                
            }
            // 如果出现1-2次，说明有问题，需要汇报
            ElseIf (%tDocType="C0019")&&(tCurErr="基数错误-重复")&&($$$EndWith(tSub,"section|entry|DE08.50.042.00"))&&('$$$BeginWith($P($G(^||DocCheckErr(%tDocSN,tSub)),"^",2),"出现3次"))
            {
                Set ^HCC.DocCheckLog(%tDocType,%tDocSN,tSub) = "基数错误-缺失^仅"_$P($G(^||DocCheckErr(%tDocSN,tSub)),"^",2)
            } 
            Else
            {
                merge ^HCC.DocCheckLog(%tDocType,%tDocSN,tSub) = ^||DocCheckErr(%tDocSN,tSub)
            }

            Set tCurErrOutput=tCurErr
            If ($P(tSub,"|",$L(tSub,"|"))="entry") || ($P(tSub,"|",$L(tSub,"|")-1)="entry")
            {
                Set tCurErrOutput="条目"_$CASE(tCurErr,"基数错误-缺失":"缺失","基数错误-重复":"重复","非标文档元素":"非标:",:tCurErr)
            }

            Set tErr={}
            Set tErr.ErrType=tCurErrOutput
            Set tErr.ErrPath=tSub
            Set tErr.ErrMsg =$P($G(^||DocCheckErr(%tDocSN,tSub)),"^",2)
            Do tErrJson.Msg.%Set(tErrCount,tErr)
            Set tErrCount = tErrCount+1
        }

        Set tPreErr = tCurErr
        Set tSub=$O(^||DocCheckErr(%tDocSN,tSub))
    } 
    
    Set tErrJson.ErrCount = tErrCount

    // for debug purpose    
    //k ^QP1
    //merge ^QP1= ^||TempTree    

    Set tRet = tErrJson.%ToJSON()
    }
    catch (ex)
    {
        Set tRet = ex.AsStatus()
    }

    Quit tRet
}

ClassMethod TestMe()
{
    Set stream=##class(%Stream.FileCharacter).%New()
    Set sc=stream.LinkToFile("/Users/qiaopeng/irishealth/HCC/sampleDoc/EMR01.xml")
    Set stream.TranslateTable="UTF8"
    w ..CheckDocStream(stream)
}

/// <p><b>校验文档字符流</b>
/// <br> pStream: 互联互通文档XML字符流。
/// <br> pCheckDisplayName: 是否检查文档元素的@displayName，默认不检查
/// <br> 文档校验错误信息保存在^HCC.DocCheckLog,第一维下标是文档编号
/// 它基于文档模版global。这些global命名规范为：^HCC.DocTemplate.<文档类型>， 例如^HCC.DocTemplate.C0032
/// 使用示例：
/// <br> Do ##class(HCC.Utils.DocValid).CheckDocStream(pXMLStream,1)
ClassMethod CheckDocStream(pStream As %Stream.Object, pCheckDisplayName As %Boolean = 0) As %String
{
    #define EndWith(%1,%2) ($E(%1,($L(%1)-$L(%2)+1),$L(%1))=%2)
    #define BeginWith(%1,%2) ($E(%1,1,$L(%2))=%2)
    Set tRet = ""
    Try
    {
    // 不检查@displayName
    Set %tCheckDisplayName = pCheckDisplayName
    

    Set tLen = 32000000
    Set tXMLStr=pStream.Read(.tLen)
    Set tJSONStr= ..XML2JSON(tXMLStr)

    Set tDocJSONObj = {}.%FromJSON(tJSONStr)

    /// 获得文档类型和文档序列号，并使其变量全局可用
    /// %tDocType：文档类型
    /// %tDocSN：文档序列号
    /// %tCurrentSection: 当前处理的章节
    /// %tDocTemplate： 文档模版名
    Set %tDocType = tDocJSONObj.ClinicalDocument.code."@code", %tDocSN = tDocJSONObj.ClinicalDocument.id."@extension"
    Set tRegex=##class(%Regex.Matcher).%New("C{1}0{2}([0-9]{2})")
    Set tRegex.Text=%tDocType
    Set tCorrectDocType = tRegex.Locate()
    If tCorrectDocType
    {
        Set tDocTypeNumber = +tRegex.SubstituteIn("$1")
        Set:((tDocTypeNumber>53) || (tDocTypeNumber<1)) tCorrectDocType = 0
    }
    W:(tCorrectDocType = 0) "错误的文档类型：",%tDocType,!
    // 获得文档模版global名，并使其变量全局可用
    Set %tDocTemplate = "^HCC.DocTemplate."_%tDocType
    K ^||TempTree,^||DocCheckErr

    Set %tCurrentSection=""

    // 遍历文档，并按模版检查文档元素
    Do ..LoopCheckDocument(tDocJSONObj,"")

    // 遍历模版，并检查文档元素Cardinality
    Set tSub= ""
    Do
    {
        Set tSub=$O(@%tDocTemplate@(tSub))

        // 不检查以下元素的Cardinality
        Set tNeedCheck=1, tSkipElements = "code|value|name|qualifier|performer|effectiveTime"       
        If $F(tSkipElements,$P(tSub,"|",$L(tSub,"|")))>0
        {
            Set tNeedCheck = 0
        }
        Continue:(tNeedCheck = 0)

        Set tAskedCard = $G(@%tDocTemplate@(tSub,"Card")),tActualCard = $G(^||TempTree(tSub,"Card"))
        Set:(tAskedCard="") tAskedCard="1..1" 
        Set tAskedMin = $Case(tAskedCard,"0..1":0,"0..*":0,:1)
        Set tAskedMax = $Case(tAskedCard,"0..*":100,"1..*":100,:1)
        Set:(tActualCard="") tActualCard=0

        If (tActualCard<tAskedMin)
        {
            Set ^||DocCheckErr(%tDocSN,tSub) = "基数错误-缺失^缺失:"_tSub
        }
        Elseif (tActualCard>tAskedMax)
        {
            Set ^||DocCheckErr(%tDocSN,tSub) = "基数错误-重复^出现"_tActualCard_"次, 超过要求的："_tAskedCard
        }
    
    } While tSub '=""
    

    // 输出错误信息
    ;k ^HCC.DocCheckLog
    Set tSub= $O(^||DocCheckErr(%tDocSN,"")), (tPreErr,tCurErr) = "", tErrCount=0, tErrJson={}
    set:(tSub'="") tErrJson.Msg=[]
    While tSub '=""
    {
        Set tCurErr = $P($G(^||DocCheckErr(%tDocSN,tSub)),"^")
        
        If (((tCurErr="基数错误-缺失")||(tCurErr="基数错误-重复")||(tCurErr="非标文档元素")) && (tCurErr '= tPreErr)) || (tCurErr="文档元素错误")||(tCurErr="数据元错误")
        {
            // 特殊处理：C0019的器械物品核对章节，无法区别"术前"、"关前"、"关后", 因此会出现3次，这是正常的。无需处理
            If (%tDocType="C0019")&&(tCurErr="基数错误-重复")&&($$$EndWith(tSub,"section|entry|DE08.50.042.00"))&&($$$BeginWith($P($G(^||DocCheckErr(%tDocSN,tSub)),"^",2),"出现3次"))
            {                
            }
            // 如果出现1-2次，说明有问题，需要汇报
            ElseIf (%tDocType="C0019")&&(tCurErr="基数错误-重复")&&($$$EndWith(tSub,"section|entry|DE08.50.042.00"))&&('$$$BeginWith($P($G(^||DocCheckErr(%tDocSN,tSub)),"^",2),"出现3次"))
            {
                Set ^HCC.DocCheckLog(%tDocType,%tDocSN,tSub) = "基数错误-缺失^仅"_$P($G(^||DocCheckErr(%tDocSN,tSub)),"^",2)
            } 
            Else
            {
                merge ^HCC.DocCheckLog(%tDocType,%tDocSN,tSub) = ^||DocCheckErr(%tDocSN,tSub)
            }

            Set tCurErrOutput=tCurErr
            If ($P(tSub,"|",$L(tSub,"|"))="entry") || ($P(tSub,"|",$L(tSub,"|")-1)="entry")
            {
                Set tCurErrOutput="条目"_$CASE(tCurErr,"基数错误-缺失":"缺失","基数错误-重复":"重复","非标文档元素":"非标:",:tCurErr)
            }

            Set tErr={}
            Set tErr.ErrType=tCurErrOutput
            Set tErr.ErrPath=tSub
            Set tErr.ErrMsg =$P($G(^||DocCheckErr(%tDocSN,tSub)),"^",2)
            Do tErrJson.Msg.%Set(tErrCount,tErr)
            Set tErrCount = tErrCount+1
        }

        Set tPreErr = tCurErr
        Set tSub=$O(^||DocCheckErr(%tDocSN,tSub))
    } 
    
    Set tErrJson.ErrCount = tErrCount

    // for debug purpose    
    //k ^QP1
    //merge ^QP1= ^||TempTree    

    Set tRet = tErrJson.%ToJSON()
    }
    catch (ex)
    {
        Set tRet = ex.AsStatus()
    }

    Quit tRet
}

/// XML转换为JSON
ClassMethod XML2JSON(pXML As %String) [ Language = python ]
{
    import xmltodict, json
    
	#; print(pXML)
    obj = xmltodict.parse(pXML)
    return(json.dumps(obj,ensure_ascii=False))
}

/// 逐条校验文档
ClassMethod LoopCheckDocument(pObj As %Library.DynamicAbstractObject, pParentKey As %String) As %Status
{
    #define EndWith(%1,%2) ($E(%1,($L(%1)-$L(%2)+1),$L(%1))=%2)
    #define BeginWith(%1,%2) ($E(%1,1,$L(%2))=%2)
    Set tRet = $$$OK
    Try
    {
    /// 如果是字典类型的结果，用数据元进行值集检查
    Set (tCode,tDisplayName) = "",tNeedCheckCode=0
    If pObj."@xsi:type" = "CD"
    {
        Set tCode = pObj."@code",tDisplayName = pObj."@displayName",tNeedCheckCode = 1
    }

    /// 遍历每个属性
    /// 其中涉及到以下属性的数组（及该属性出现多次）：
    /// authenticator（医师签名 - 多个角色）
    /// participant（参与人员 - 多个角色，例如手术操作章节中的一助、二助）
    /// author（质控医护 - 多个角色，例如质控医生、质控护士）
    /// id (患者标识 - 多种标识，例如门急诊号、住院号)
    /// component（章节）
    /// entry （条目）
    /// entryRelationship（元素）
    /// component（子条目）
    set iter = pObj.%GetIterator()
    while iter.%GetNext(.key, .value) 
    { 
        Set tNeedCheck = 1,tElementkey=""

        /**Step1: 建立完整元素路径**/
        // 处理章节时，使用章节的code或displayname作为名称
        If (pParentKey="ClinicalDocument|component|structuredBody|component") && (+key=key)
        {
            Set tElementkey = $Case(value.section.code."@code","":value.section.code."@displayName",:value.section.code."@code")
            Set %tCurrentSection = tElementkey

            // 特殊处理：
            // 在C0049中，入院诊断章节和出院诊断章节都使用11535-2这个LOINC code，需要用@displayName区分
            If (%tDocType="C0049") && (tElementkey="11535-2")
            {
                Set tElementkey = value.section.code."@displayName"
            }
        }
        // 处理患者Id时，用ID类型作为名称
        Elseif (pParentKey="ClinicalDocument|recordTarget|patientRole|id") && (+key=key)
        {
            Set tElementkey = value."@root"
        }        
        // 处理条目子条目数组（一个条目有多个component元素时），优先用code.qualifier.name."@displayName"，其次code.@displayname作为名称
        Elseif $$$EndWith(pParentKey,"|organizer|component") && (+key=key)
        {
            // 特殊处理：C0032的诊断记录章节中，“门（急）诊诊断”和“病理诊断”条目的DE05.01.024.00、DE05.01.025.00的observation.code."@displayName"是一样的，无法用它区分！
            If (%tDocType="C0032") && (%tCurrentSection="29548-5") && ((value.observation.code."@code"="DE05.01.024.00")||(value.observation.code."@code"="DE05.01.025.00"))
            {
                Set tElementkey = value.observation.code."@code"
            }
            Else
            {
                Set tElementkey = value.observation.code.qualifier.name."@displayName"
            }
            Set:(tElementkey="") tElementkey = value.observation.code."@code"
            Set:(tElementkey="") tElementkey = value.observation.code."@displayName"
        }
        // 处理章节条目数组（多个条目时）
        Elseif $$$EndWith(pParentKey,"|section|entry") && (+key=key)
        {
            /// 如果是手术与处置，一般使用固定值procedure。但C0044中，“抢救措施”也是procedure条目，需要特殊处理
            If (value.procedure'="") 
            {
                If value.procedure.code."@code" = "DE06.00.094.00"
                {
                    Set tElementkey= "DE06.00.094.00"
                }
                Else
                {
                    Set tElementkey= "procedure"
                }
                
            }
            /// 其次查找entry.organizer.code."@displayName" (C0032/主要健康问题章节/颅脑损伤患者入院前昏迷时间 & 颅脑损伤患者入院后昏迷时间)
            Set:(tElementkey="") tElementkey = value.organizer.code."@displayName"
            /// 再次查找entry.observation.code.qualifier.name.@displayName
            Set:(tElementkey="") tElementkey = value.observation.code.qualifier.name."@displayName"
            /// 再次查找code."@code"
            Set:(tElementkey="") tElementkey = value.observation.code."@code"

            /// 特殊处理：
            /// C0034: code/@code=DE05.01.025.00，需要code/@display替换
            /// C0034/C0035/C0036/C0041/C0043/C0053: code/@code=DE05.10.130.00，需要code/@display替换
            /// C0034/C0035/C0036/C0049: code/@code=DE05.10.172.00，需要code/@display替换
            /// C0027: code/@code=DE06.00.018.00，需要code/@display替换
            /// C0033: code/@code=DE04.10.019.00，需要code/@display替换 (入院体重、出生体重)
            /// C0010: code/@code=DE04.30.009.00，需要code/@display替换（MRI检查结果条目、肺功能检查结果条目）
            /// C0010: code/@code=DE04.50.128.00，需要code/@display替换（血常规检查结果条目、血气分析检查结果条目）
            /// C0014: code/@code=DE04.10.188.00，需要code/@display替换（产前体重、孕前体重）
            /// C0015: code/@code=DE08.50.022.00，需要code/@display替换（麻醉药物名称、产后用药、产时用药）
            /// C0016: code/@code=DE04.10.233.00，需要code/@display替换（子宫情况、腹腔探查子宫）
            /// C0016: code/@code=DE08.50.022.00，需要code/@display替换（其他用药、宫缩剂名称、手术用药）
            /// C0042: code/@code=DE08.10.026.00，需要code/@display替换（转出科室、转入科室）
            /// C0003: code/@code=DE05.01.025.00和DE05.01.024.00，需要code/@display替换
            /// C0033: code/@code=HDSD00.12.099，需要替换为DE06.00.247.00 （因为HDSD00.12.099是数据集元素标识，而非数据元标识，C0033标准示例中写错了）
            If (tElementkey="DE05.01.025.00") && (%tDocType="C0034")
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif (tElementkey="DE05.10.130.00") && ((%tDocType="C0034")||(%tDocType="C0035")||(%tDocType="C0036")||(%tDocType="C0041")||(%tDocType="C0043")||(%tDocType="C0053"))
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif (tElementkey="DE05.10.172.00") && ((%tDocType="C0049")||(%tDocType="C0034")||(%tDocType="C0035")||(%tDocType="C0036"))
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif (tElementkey="DE04.10.019.00") && (%tDocType="C0033")
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif ((tElementkey="DE04.30.009.00") || (tElementkey="DE04.50.128.00")) && (%tDocType="C0010")
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif (tElementkey="DE08.50.022.00") && (%tDocType="C0015")
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif ((tElementkey="DE04.10.233.00") || (tElementkey="DE08.50.022.00")) && (%tDocType="C0016")
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif (tElementkey="DE08.10.026.00") && (%tDocType="C0042")
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif (tElementkey="DE04.10.188.00") && (%tDocType="C0014")
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif (tElementkey="DE06.00.018.00")
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Elseif (tElementkey="HDSD00.12.099")
            {
                Set tElementkey="DE06.00.247.00"
            }       

            /// 再次查找code."@displayName"
            Set:(tElementkey="") tElementkey = value.observation.code."@displayName"
           
            /// 再次查找entry/organizer/component[1]/observation/code/qualifier/name/@displayName(C0032/出院诊断章节)，或第一个元素的observation/code/@code
            If (tElementkey="") 
            {
                // 条目有多个子条目（component）
                If (value.organizer'="") && (value.organizer.%GetTypeOf("component")="array")
                {
                    Set tElementkey = value.organizer.component.%Get(0).observation.code.qualifier.name."@displayName"
                    Set:(tElementkey="") tElementkey = value.organizer.component.%Get(0).observation.code."@code"

                    // 特殊处理：如果是C0002、C0003的诊断记录章节的西医诊断名称和西医诊断编码，使用固定条目标识“西医诊断条目”
                    If ((tElementkey="DE05.01.025.00") || (tElementkey="DE05.01.024.00")) && ((%tDocType="C0002")||(%tDocType="C0003"))
                    {
                        Set tElementkey="西医诊断条目"
                    }
                    // 特殊处理：如果是C0007的实验室检查章节的检验项目条目，使用固定条目标识“检验项目条目”
                    ElseIf ((tElementkey="DE04.50.019.00") || (tElementkey="DE04.30.017.00") || (tElementkey="DE04.30.015.00")) && ((%tDocType="C0007"))
                    {
                        Set tElementkey="检验项目条目"
                    }
                    // 否则查找对照关系，获得多component组合条目的条目标识
                    Else
                    {
                        // 组合条目处理，获得条目标识                
                        Set tElementkey=..GetEntryId(tElementkey)
                    }
                }
                Else
                {
                    Set tElementkey = value.organizer.component.observation.code.qualifier.name."@displayName"
                    Set:(tElementkey="") tElementkey = value.organizer.component.observation.code."@code"
                    Set:(tElementkey="") tElementkey = value.observation.code."@code"
                }
            }
        }        
        // 处理条目项目数组（一个条目的多个entryRelationship项目时）
        Elseif $$$EndWith(pParentKey,"|entryRelationship") && (+key=key)
        {
            Set tElementkey = value.observation.code.qualifier.name."@displayName"
            Set:(tElementkey="") tElementkey = value.observation.code."@code"

            // 特殊处理：C0034/C0035/C0036: code/@code=DE05.10.130.00 或DE05.10.172.00 ，需要用code/@display替换
            If ((tElementkey="DE05.10.130.00")||(tElementkey="DE05.10.172.00")) && ((%tDocType="C0034")||(%tDocType="C0035")||(%tDocType="C0036"))
            {
                Set tElementkey=value.observation.code."@displayName"
            }
            Set:(tElementkey="") tElementkey = value.observation.code."@displayName"
            // C0027,治疗计划章节,拟实施手术条目，下级是procedure而非observation
            Set:((tElementkey="")&&(value.procedure'="")) tElementkey = "procedure"
            // C0019, 手术操作章节,出入手术室时间条目，没有可以获得下标信息
            Set:(tElementkey="") tElementkey = "default"
        }
        // 处理条目项目数组（一个条目的多个authenticator时）
        Elseif $$$EndWith(pParentKey,"|authenticator") && (+key=key)
        {
            // 只有assignedEntity.code."@codeSystem"为空时，@displayName才是固定值；否则，@displayName是代理人与患者关系！
            Set:(value.assignedEntity.code."@codeSystem"="") tElementkey = value.assignedEntity.code."@displayName"
            // 如果有value.assignedEntity.code，说明是代理人
            // C0026,C0028,C0029,C0030,C0031 “代理人”没有"@displayName"
            Set:((tElementkey ="")&&(value.assignedEntity.code'="")) tElementkey ="代理人"
            // 否则是患者本人，无需设置（保持tElementkey为空）
        }
        // 处理条目项目数组（一个条目的多个participant时(例如C0032>手术操作章节)）
        Elseif $$$EndWith(pParentKey,"|participant") && (+key=key)
        {
            Set tElementkey = value.participantRole.code."@displayName"
            Set:(tElementkey ="") tElementkey = value.associatedEntity.code."@displayName"
            Set:(tElementkey ="") tElementkey = value."@typeCode"
        }
        // 处理条目项目数组（一个条目的多个author时(C0032>行政管理章节)）
        Elseif $$$EndWith(pParentKey,"|author") && (+key=key)
        {
            Set tElementkey = value.assignedAuthor.code."@displayName"
        }
        Elseif (key = "#text")
        {
            Set tElementkey = ""
        }
        Else
        {
            // 非数组！
            If (key="entryRelationship") && value.%IsA("%Library.DynamicObject")
            {
                Set tElementkey = value.observation.code.qualifier.name."@displayName"
                Set:(tElementkey="") tElementkey = value.observation.code."@code"
                Set:(tElementkey="") tElementkey = value.observation.code."@displayName"

                If (tElementkey'="")
                {
                    Set tElementkey = key_"|"_tElementkey
                }
                Else
                {
                    Set tElementkey = key
                }
            }
            ElseIf (key="entry") && value.%IsA("%Library.DynamicObject")
            {
                Set tElementkey = value.observation.code."@code"
                Set:(tElementkey="") tElementkey = value.observation.code."@displayName"
                Set:((tElementkey="")&&(value.procedure'="")) tElementkey = "procedure"

                // 从第一个component中获得数据
                If (tElementkey="")
                {
                    If (value.organizer'="") && (value.organizer.%GetTypeOf("component")="array")
                    {
                        Set tElementkey = value.organizer.component.%Get(0).observation.code.qualifier.name."@displayName"
                        Set:(tElementkey="") tElementkey = value.organizer.component.%Get(0).observation.code."@code"

                        // 特殊处理：如果是C0002、C0003的诊断记录章节的西医诊断名称和西医诊断编码，
                        If ((tElementkey="DE05.01.025.00") || (tElementkey="DE05.01.024.00")) && ((%tDocType="C0002")||(%tDocType="C0003"))
                        {
                            Set tElementkey="西医诊断条目"
                        }
                        // 否则查找对照关系，获得多component组合条目的条目标识
                        Else
                        {
                            // 组合条目处理，获得条目标识                
                            Set tElementkey=..GetEntryId(tElementkey)
                        }
                    }
                    Else
                    {
                        Set tElementkey = value.organizer.component.observation.code.qualifier.name."@displayName"
                        Set:(tElementkey="") tElementkey = value.organizer.component.observation.code."@code"
                    }
                }

                //特殊处理：C0032的过敏史章节！
                Set:(tElementkey="") tElementkey = value.act.entryRelationship.observation.code."@code"               

                If (tElementkey'="")
                {
                    Set tElementkey = key_"|"_tElementkey
                }
                Else
                {
                    Set tElementkey = key
                }
            }
            ElseIf (key="authenticator") && value.%IsA("%Library.DynamicObject")
            {
                // 只有assignedEntity.code."@codeSystem"为空时，@displayName才是固定值；否则，@displayName是代理人与患者关系！
                Set:(value.assignedEntity.code."@codeSystem"="") tElementkey = value.assignedEntity.code."@displayName"
                // 特殊处理：C0048中被错误放在了assignedEntity.code."@code"中，需要将错就错！
                Set:(%tDocType="C0048") tElementkey = value.assignedEntity.code."@code"
                If (tElementkey'="")
                {
                    Set tElementkey = key_"|"_tElementkey
                }
                Else
                {
                    Set tElementkey = key
                }
            }
            // added by QP on 2023/4/8, to set subscription for patientRole/id
            Elseif (key="id") && (pParentKey="ClinicalDocument|recordTarget|patientRole") && (value'="") && value.%IsA("%Library.DynamicObject") 
            {
                Set tElementkey = key_"|"_value."@root"
            }
            // added by QP on 2023/4/17, 针对条目子条目（entry/component）
            ElseIf (key="component") && (pParentKey'="ClinicalDocument") && (pParentKey'="ClinicalDocument|structuredBody")&& value.%IsA("%Library.DynamicObject")
            {
                Set tElementkey = key_"|"_value.observation.code."@code"
            }
            Else
            {
                Set tElementkey = key
            }
        }
        //（）半角替换全角
        Set tElementkey = $REPLACE($REPLACE(tElementkey,"（","("),"）",")")

        If (pParentKey'="") 
        {
            Set tFullKey = pParentKey
            Set:(tElementkey'="") tFullKey = tFullKey_"|"_tElementkey
        }
        Else
        {
            Set tFullKey = tElementkey
        }
        
        /**Step2: 对照模版，检查元素的类型、值和值集约束**/
        //不检查以下元素
        If (tFullKey="ClinicalDocument") || (tFullKey="ClinicalDocument|component") || (tFullKey="ClinicalDocument|component|structuredBody")|| (tFullKey="ClinicalDocument|component|structuredBody|component")||(tFullKey="ClinicalDocument|authenticator")
        {
            Set tNeedCheck = 0
        }       
        ElseIf (pObj.%GetTypeOf(key)="array") && ((key="component")||(key="entryRelationship")||(key="entry")||(key="id")||(key="participant")||(key="author"))
        {
            Set tNeedCheck = 0
        }
        ElseIf (key = "@xmlns:mif")||(key = "@xmlns:xsi")||(key = "@xmlns")||(key = "@contextConductionInd")
        {
            Set tNeedCheck = 0
        }
        ElseIf (%tCheckDisplayName=0) && ((key = "@displayName")||(key = "@codeSystemName"))
        {
            Set tNeedCheck = 0
        }

        // 检查文档模版，报告错误
        If tNeedCheck = 1
        {            
            // 增加Cardinality计数(不对#text计数！)
            If (key'="#text") 
            {
                // 之前已经存在该标签
                If $G(^||TempTree(tFullKey,"Card")) '=""
                {
                    // 如果是条目、子条目或元素的根标签重复出现（即数组），检查之前的条目/子条目/元素其所有子元素的基数是否正确
                    If (+key=key) 
                    {
                        //检查之前的条目/子条目/元素的基数是否正确                        
                        Set tSub= tFullKey
                        // 遍历所有子元素
                        Do
                        {
                            // 获得之前条目/子条目/元素的所有元素的出现的次数
                            Set tAskedCard = $G(@%tDocTemplate@(tSub,"Card")),tActualCard = $G(^||TempTree(tSub,"Card"))
                            Set:(tAskedCard="") tAskedCard="1..1" 
                            Set:(tActualCard="") tActualCard=0

                            Set tAskedMin = $Case(tAskedCard,"0..1":0,"0..*":0,:1)
                            Set tAskedMax = $Case(tAskedCard,"0..*":100,"1..*":100,:1)                   

                            // 如果正在检查的标签就是开始重复的标签，则实际值=当前值+1
                            If (tSub=tFullKey)
                            {
                                Set:(tSub=tFullKey) tActualCard=tActualCard+1
                            }
                            // 其他元素标签的基数记录清零
                            Else
                            {
                                // 清除之前的基数记录
                                k ^||TempTree(tSub,"Card")
                            }
                            
                            // 如果实际出现次数低于要求的Cardinality，说明该元素未出现
                            If tActualCard<tAskedMin
                            {
                                Set ^||DocCheckErr(%tDocSN,tSub) = "基数错误-缺失^缺失:"_tSub
                            }
                            ElseIf tActualCard>tAskedMax
                            {
                                Set ^||DocCheckErr(%tDocSN,tSub) = "基数错误-重复^出现"_tActualCard_"次, 超过要求的："_tAskedCard
                            }

                            // 遍历下一个标签
                            Set tSub=$O(@%tDocTemplate@(tSub))
                        } While ((tSub '="") && ($$$BeginWith(tSub,tFullKey)))
                    }
                    else
                    {
                        ;w "非预期项目：",tFullKey," key:",key,":",value,!
                    }
                    
                }
            }           
            
            // 校验元素的是否定义在模版中
            If $D(@%tDocTemplate@(tFullKey))
            {
                If (pObj.%GetTypeOf(key)="string") 
                {
                    Set tTargetValue = $G(@%tDocTemplate@(tFullKey,"Value")),tCorrect=1
                    // 数据元约束校验
                    If $E(tTargetValue,1) = "{"
                    {
                        //获得数据元
                        Set tMsg = "",tDE = $E(tTargetValue,2,$L(tTargetValue)-1)
                        //如果是@code元素，且这是一个编码的数据,进行数据元值集校验
                        If (tNeedCheckCode=1) && (key="@code")
                        {
                            If ##class(WS.DE.Tools).ValidateAll(tDE,tCode,tDisplayName,.tMsg) = 0
                            {
                                Set ^||DocCheckErr(%tDocSN,tFullKey) = "数据元错误^数据元"_tDE_"值集错误："_tMsg
                            }
                        }
                        //进行数据元值校验
                        else
                        {
                            If ##class(WS.DE.Tools).ValidateDisplay(tDE,value,.tMsg) = 0
                            {
                                Set ^||DocCheckErr(%tDocSN,tFullKey) = "数据元错误^数据元"_tDE_"值错误："_tMsg
                            }
                        }
                        
                    }
                    // 非数据元元素，检查值约束
                    Else
                    {
                        //直接比较结果
                        Set:((tTargetValue'="") && (tTargetValue '= value)) tCorrect=0
                    }
                    If (tCorrect=0) 
                    { 
                        Set ^||DocCheckErr(%tDocSN,tFullKey) = "文档元素错误^"_key_"值为"_value_" 不正确，应为 "_tTargetValue
                    }
                }
            }
            Else
            {
                Set ^||DocCheckErr(%tDocSN,tFullKey) = "非标文档元素^模版中不存在该文档元素"
            }
        }

        /**Step3: 记录元素出现的次数**/
        // 不记录"#text"，和entry数组根元素
        If (key="#text") ||((key="entry")&& (value.%IsA("%DynamicArray")))
        {
        }
        Else
        {
            Set ^||TempTree(tFullKey,"Card") = $G(^||TempTree(tFullKey,"Card"))+1
        }

        // 嵌套调用下一级
        If (pObj.%GetTypeOf(key)="object") || (pObj.%GetTypeOf(key)="array")
        {
            Do ..LoopCheckDocument(value,tFullKey)
        }
        
    }
    }
    catch (ex)
    {
        Set tRet = ex.AsStatus()
    }
    Quit tRet
}

/// 条目标识处理
/// 很多条目由多个子条目（component）构成，例如血压由舒张压和收缩压2个component组成，且component出现顺序不定
/// 此方法根据任何子条目编码返回正确的条目标识
/// 未匹配项，直接返回原值
ClassMethod GetEntryId(pComponentCode As %String) As %String [ CodeMode = expression ]
{
$CASE(pComponentCode,
    "DE04.50.001.00":"血型条目",
    "DE04.50.010.00":"血型条目",

    "DE04.10.174.00":"血压条目",
    "DE04.10.176.00":"血压条目",

    "DE06.00.092.00":"出入院时间条目",
    "DE06.00.017.00":"出入院时间条目",

    "DE04.30.010.00":"辅助检查项目条目",
    "DE04.30.009.00":"辅助检查项目条目",

    "DE06.00.288.00":"医嘱条目",
    "DE06.00.289.00":"医嘱条目",

    "DE04.30.019.00":"检验项目条目",
    "DE04.30.017.00":"检验项目条目",
    "DE04.30.015.00":"检验项目条目",

    "DE05.10.138.01":"颅脑损伤患者入院前昏迷时间",
    "DE05.10.138.02":"颅脑损伤患者入院前昏迷时间",
    "DE05.10.138.03":"颅脑损伤患者入院前昏迷时间",

    "HDSD00.12.172":"综合医疗服务费",
    "HDSD00.12.173":"综合医疗服务费",
    "HDSD00.12.174":"综合医疗服务费",
    "HDSD00.12.175":"综合医疗服务费",
    "HDSD00.12.176":"综合医疗服务费",
    "HDSD00.12.177":"综合医疗服务费",

    "HDSD00.12.136":"诊断类服务费",
    "HDSD00.12.137":"诊断类服务费",
    "HDSD00.12.138":"诊断类服务费",
    "HDSD00.12.139":"诊断类服务费",

    "HDSD00.12.145":"治疗类服务费",
    "HDSD00.12.147":"治疗类服务费",

    "HDSD00.12.153":"中医类费用",
    "HDSD00.12.156":"中医类费用",
    "HDSD00.12.157":"中医类费用",

    "HDSD00.12.150":"中药费用",
    "HDSD00.12.151":"中药费用",

    "HDSD00.12.126":"血液和血液制品类服务费",
    "HDSD00.12.127":"血液和血液制品类服务费",
    "HDSD00.12.128":"血液和血液制品类服务费",
    "HDSD00.12.129":"血液和血液制品类服务费",
    "HDSD00.12.130":"血液和血液制品类服务费",

    "HDSD00.12.045":"耗材类费用",
    "HDSD00.12.046":"耗材类费用",
    "HDSD00.12.047":"耗材类费用",
    
    "中医证候名称":"中医证候条目",
    "中医证候代码":"中医证候条目",
    "中医病名名称":"中医病名条目",
    "中医病名代码":"中医病名条目",
    "中医诊断名称":"中医病名条目",
    "中医诊断代码":"中医病名条目",
    "病理诊断名称":"病理诊断条目",
    "病理诊断代码":"病理诊断条目",
    "西医诊断名称":"西医诊断条目",
    "西医诊断代码":"西医诊断条目",
    "诊断名称":"西医诊断条目",
    "诊断代码":"西医诊断条目",

    "主病名称":"中医病名条目",
    "主病代码":"中医病名条目",
    "主证名称":"中医证候条目",
    "主证代码":"中医证候条目",
    "主要诊断-疾病名称":"主要诊断",
    "主要诊断疾病编码":"主要诊断",
    "主要诊断名称":"主要诊断",
    "主要诊断疾病编码":"主要诊断",
    "其他诊断名称":"其他诊断",
    "其他诊断疾病编码":"其他诊断",

    :pComponentCode
    )
}

}
